package com.mvu.banana.tools;

import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.user.client.ui.ListBox;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

public class FormDoclet {

  private static String outputFolder = "src/main/java";

  public static boolean start(RootDoc root) {
    //iterate over all classes.
    for (ClassDoc classDoc : root.classes()) {
      if (classDoc.tags("generate-server-code").length > 0) {
        out("Processing...\t" + classDoc.name());
        JavaClass dto = genDTO(classDoc);
        JavaClass servlet = genServlet(classDoc, dto);
      }
    }
    return true;
  }

  private static JavaClass genServlet(ClassDoc classDoc, JavaClass dto) {
    JavaClass stub = new JavaClass()
            .withName(classDoc.qualifiedName().replace(".client.", ".server.gen.") + "ServletStub")
            .withModifier("abstract")
            .withSuperClass("com.mvu.banana.common.server.AJAXServlet", dto.getFullName());
    addServerMethods(stub, dto);
    stub.write(outputFolder, true);
    JavaClass servlet = new JavaClass()
            .withName(classDoc.qualifiedName().replace(".client.", ".server.") + "Servlet")
            .withSuperClass(stub);
    servlet.write(outputFolder, false);
    return stub;
  }

  private static JavaClass genDTO(ClassDoc classDoc) {
    String dtoName = classDoc.qualifiedName().replace(".client.", ".client.gen.") + "DTO";
    JavaClass dto = new JavaClass()
            .withName(dtoName)
            .withSuperClass("com.mvu.banana.common.client.ValueDTO", classDoc.qualifiedName())
            .withMappingInfo(classDoc);
    for (FieldDoc field : classDoc.fields()) {
      SeeTag[] inputs = field.seeTags();
      Tag[] outputs = field.tags("output");
      if (inputs.length + outputs.length > 0) {
        Class type = String.class;
        Field beanField = null;
        Class beanClass = null;
        if (inputs.length > 0) {
          try {
            beanClass = Class.forName(inputs[0].referencedClassName());
            type = beanClass;
            String memberName = inputs[0].referencedMemberName();
            if (memberName != null) {
              beanField = beanClass.getDeclaredField(memberName);
              type = beanField.getType();
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
        dto.addField().withName(field.name())
                .withType(type).withModifier("public")
                .withMappingInfo(field, beanClass, beanField);
      }
    }
    dto.addConstructor();
    addPrepareMethod(dto);
    addValidateMethod(dto);
    addClientMethods(dto);
    dto.write(outputFolder, true);
    return dto;
  }

  private static void addPrepareMethod(JavaClass javaClass) {
    ClassDoc form = (ClassDoc) javaClass.info[0];
    JavaMethod prepareMethod = javaClass.addMethod().withName("prepare")
            .withParameters(form.qualifiedName());
    prepareMethod.addAnnotation(Override.class);
    for (JavaField field : javaClass.fields) {
      String fieldName = field.name;
      JavaType type = field.type;
      FieldDoc fieldDoc = (FieldDoc) field.info[0];
      String widgetType = fieldDoc.type().qualifiedTypeName();
      if (widgetType.endsWith("Box")) { // TODO: do better
        javaClass.importClass(widgetType);
        prepareMethod.addLine("if(p0.", fieldName, " == null){\n      p0.", fieldName,
                " = new ", fieldDoc.type().typeName(), "(",
                widgetType.endsWith("EnumBox") ? type.elementName + ".class" : ""
                ,");\n    }");
        prepareMethod.addLine("p0.", fieldName, ".setName(\"", fieldName, "\")");
      }
      String title = new StringBuilder(form.name()).append("_").append(fieldName).toString();
      if (fieldDoc.tags("hint").length > 0) {
        javaClass.importClass("com.mvu.banana.common.widget.HintListener");
        prepareMethod.addLine("p0.", fieldName, ".setTitle(\"", title, "\")");
        prepareMethod.addLine("p0.", fieldName, ".addFocusHandler(HintListener.get())");
        prepareMethod.addLine("p0.", fieldName, ".addBlurHandler(HintListener.get())");
      }
    }
  }

  private static void addValidateMethod(JavaClass javaClass) {
    JavaMethod validateMethod = null;
    for (JavaField field : javaClass.fields) {
      String fieldName = field.name;
      String checkExpression = getCheckExpression(field);
      if (checkExpression != null) {
        if (validateMethod == null) {
          javaClass.importClass("static com.mvu.banana.common.validation.Validator.*");
          String formClass = ((ClassDoc) javaClass.info[0]).qualifiedName();
          validateMethod = javaClass.addMethod(false, "boolean", "validate", formClass);
          validateMethod.addLine("boolean isValid = true");
        }
        validateMethod.addLine("boolean ", fieldName, "IsValid = ", checkExpression);
        validateMethod.addLine("p0.highlight(p0.", fieldName, ", ", fieldName, "IsValid)");
        validateMethod.addLine("isValid &= ", fieldName, "IsValid");
      }
    }
    if (validateMethod != null) {
      validateMethod.addLine("return isValid");
    }
  }

  private static String getCheckExpression(JavaField field) {
    Annotation[] beanAnnotations = field.info[2] == null ? new Annotation[0]
            : ((Field) field.info[2]).getAnnotations();
    int constraintsCount = 0;
    StringBuilder builder = new StringBuilder();
    for (Annotation annotation : beanAnnotations) {
      Class annotationClass = annotation.annotationType();
      String className = annotationClass.getName();
      if (className.contains("constraints")) {
        if (constraintsCount > 0) {
          builder.append(" && ");
        }
        builder.append("is").append(annotationClass.getSimpleName()).append("(")
                .append(field.name);
        if (annotationClass.equals(Min.class)) {
          builder.append(", ").append(((Min) annotation).value());
        } else if (annotationClass.equals(Max.class)) {
          builder.append(", ").append(((Max) annotation).value());
        } else if (annotationClass.equals(Length.class)) {
          Length length = (Length) annotation;
          builder.append(", ").append(length.min()).append(", ").append(length.max());
        }
        builder.append(")");
        constraintsCount++;
      }
    }
    Tag[] annotations = ((FieldDoc) field.info[0]).tags();
    for (Tag tag : annotations) {
      String tagName = tag.name().substring(1);
      if (tagName.equals("NotNull")) {
        if (constraintsCount > 0) {
          builder.append(" && ");
        }
        builder.append("is").append(tagName).append("(")
                .append(field.name);
        builder.append(")");
        constraintsCount++;
      }
    }
    return constraintsCount > 0 ? builder.toString() : null;
  }

  /**
   * Return null if is not user input field
   */
  private static String getValueExpression(JavaField field) {
    StringBuilder builder = new StringBuilder();
    String inputType = ((FieldDoc) field.info[0]).type().qualifiedTypeName();
    String fieldName = field.name;
    if (SpanElement.class.getName().equals(inputType)) {
      return null; // this is not user input field
    } else if (ListBox.class.getName().equals(inputType)) {
      builder.append(field.type.elementName).append(".valueOf(")
              .append("p0.").append(fieldName).append(".getValue(p0.")
              .append(fieldName).append(".getSelectedIndex()))");
    } else if (inputType.endsWith("Box")) {
      builder.append("p0.").append(fieldName).append(".getValue()");
    } else {
      builder.append("p0.").append(fieldName);
    }
    return builder.toString();
  }

  private static String setValueExpression(JavaField field) {
    StringBuilder builder = new StringBuilder();
    String inputType = ((FieldDoc) field.info[0]).type().qualifiedTypeName();
    String fieldName = field.name;
    if (SpanElement.class.getName().equals(inputType)) {
      builder.append(".setInnerText(this.").append(fieldName).append(")");
    } else if (ListBox.class.getName().equals(inputType)) {
      builder.append(".setSelectedIndex(this.").append(fieldName);
      if (field.type.isEnum) {
        builder.append(".ordinal() + 1)");
      } else {
        builder.append(")");
      }
    } else if (inputType.endsWith("Box")) {
      builder.append(".setValue(this.").append(fieldName).append(")");
    } else {
      builder.append(" = this.").append(fieldName);
    }
    return builder.toString();
  }

  private static void addClientMethods(JavaClass javaClass) {
    String formClass = ((ClassDoc) javaClass.info[0]).qualifiedName();
    JavaMethod updateMethod = null;
    JavaMethod copyMethod = null;
    for (JavaField field : javaClass.fields) {
      FieldDoc fieldDoc = (FieldDoc) field.info[0];
      if (fieldDoc.tags("output").length > 0) {
        if(updateMethod == null){
          updateMethod = javaClass.addMethod().withName("update").withParameters(formClass);
          updateMethod.addAnnotation(Override.class);
        }
        String setValueExpression = setValueExpression(field);
        updateMethod.addLine("p0.", field.name, setValueExpression);
      }
      if (fieldDoc.seeTags().length > 0) {
        if(copyMethod == null){
          copyMethod = javaClass.addMethod().withName("copy").withParameters(formClass);
          copyMethod.addAnnotation(Override.class);
        }
        String getValueExpression = getValueExpression(field);
        copyMethod.addLine("this.", field.name, " = ", getValueExpression);
      }
    }
  }

  private static void addServerMethods(JavaClass javaClass, JavaClass dto) {
    Map<String, JavaMethod> createBeanMethods = new HashMap<String, JavaMethod>();
    Map<String, JavaMethod> copyBeanMethods = new HashMap<String, JavaMethod>();
    for (JavaField field : dto.fields) {
      if (field.info[2] != null) {
        Field beanField = (Field) field.info[2];
        if (beanField != null) {
          String fieldName = beanField.getName();
          fieldName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
          Class entityClass = (Class) field.info[1];
          String entityName = entityClass.getSimpleName();
          JavaMethod createBeanMethod = createBeanMethods.get(entityName);
          JavaMethod copyBeanMethod = copyBeanMethods.get(entityName);
          if (createBeanMethod == null) {
            javaClass.importClass(entityClass);
            createBeanMethod = javaClass.addMethod().withName("create", entityName)
                    .withParameters(dto)
                    .withReturnType(entityName);
            createBeanMethods.put(entityName, createBeanMethod);
            createBeanMethod.addLine(entityName, " bean = new ", entityName, "()");
            createBeanMethod.addLineAtEnd("return bean");
            copyBeanMethod = javaClass.addMethod()
                    .withName("copy").withParameters(entityName, dto);
            copyBeanMethods.put(entityName, copyBeanMethod);
          }
          createBeanMethod.addLine("bean.set", fieldName, "(p0.", field.name, ")");
          copyBeanMethod.addLine("p1.", field.name, " = p0.get", fieldName, "()");
        }
      }
    }
  }

  private static void out(String msg) {
    System.out.println(msg);
  }

  private static MessageFormat METHODINFO =
          new MessageFormat("Method: return type {0}, name = {1};");
  private static MessageFormat FIELDINFO =
          new MessageFormat("Field: name = {0}, comment = {1}, type = {2};");

  public static void main(String[] args) {
    String[] params = {"-doclet", FormDoclet.class.getName(),
            "-sourcepath", outputFolder,
//            "src/main/java/com/mvu/banana/guest/client/SignUpForm.java"};
            "com.mvu.banana.guest.client"};
    com.sun.tools.javadoc.Main.main(params);
  }
}

